package neuronvisualizer;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;

import com.threed.jpct.Object3D;
import com.threed.jpct.Primitives;
import com.threed.jpct.SimpleVector;
import com.threed.jpct.Texture;
import com.threed.jpct.TextureManager;
import java.awt.Graphics2D;
import java.awt.geom.RectangularShape;
import org.jfree.ui.RectangleEdge;

import org.jfree.chart.*;
import org.jfree.chart.renderer.xy.XYBarPainter;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.data.statistics.HistogramDataset;
import org.jfree.chart.plot.PlotOrientation;
import java.io.IOException;
import java.io.File;

public class Driver implements ActionListener {
	
	private final float LOD_DISTANCE = 1500; //The maximum distance from the camera a section will be fully rendered at
	public final String[] layerTitles = {"II/III","IV","V","VI"};
	
	private Neuron[] neuronList;
	private Connection[] connectionList;
	private Section[][] sections;
	private ArrayList<Object3D> extraModels;
	private ArrayList<Connection> visibleConnections;
	
	private Integer pickedNeuron;
	
	private float maxX;
	private float minX;
	private float maxY;
	private float minY;
	private float maxZ;
	private float minZ;
	
	private float scale;	
	public float maxCharge;
	public float minCharge;
	
	private boolean showingRS;
	private boolean showingLTS;
	private boolean showingFS;
	private boolean showingIB;
	
    //GUI COMPONENTS
	private JFreeChart histogram;
    private OptionBar options;
    private Visual showNetwork;
    private AnalysisBar analysis;
    private SimpleVector midPoint;
    private TimelineGUI timeGUI;
   
	public boolean hidingAll = false;
	private boolean initialized;
	private int connectionThreshold;
    
	
	
	private Object3D selected;
	
	public static void main(String[] args){
		new Driver();
	}
	
	
	public Driver(){
		initialized = false;
		scale = 1f;
		extraModels = new ArrayList<Object3D>();
		visibleConnections = new ArrayList<Connection>();
		loadTextures();
		options = new OptionBar(this);
		selected = null;
		
		showingIB = true;
		showingRS = true;
		showingLTS = true;
		showingFS = true;
		showAnalysis();
	}
	
	public void loadTextures(){
		TextureManager.getInstance().addTexture("blue",new Texture(10,10,new Color(181,221,255)));
		TextureManager.getInstance().addTexture("white",new Texture(10,10,new Color(255,255,255)));
		TextureManager.getInstance().addTexture("gray",new Texture(10,10,new Color(164,164,164)));
		TextureManager.getInstance().addTexture("purple",new Texture(10,10,new Color(250,108,248)));
		TextureManager.getInstance().addTexture("red",new Texture(10,10,new Color(228,4,4)));
		TextureManager.getInstance().addTexture("dblue",new Texture(10,10,new Color(0,97,172)));
		TextureManager.getInstance().addTexture("bblue",new Texture(10,10,new Color(27,175,224)));
		TextureManager.getInstance().addTexture("green",new Texture(10,10,new Color(54,220,38)));
		TextureManager.getInstance().addTexture("yellow",new Texture(10,10,new Color(255,255,0)));
		TextureManager.getInstance().addTexture("glow",new Texture(this.getClass().getResourceAsStream("resource/glow.png"),true));	
		TextureManager.getInstance().addTexture("select",new Texture(this.getClass().getResourceAsStream("resource/select.png"),true));		
	}	
	public int getSectionColumnCount(){
		return sections[0].length;
	}
	
	public int getNeuronCount(){
		return neuronList.length;
	}
	
	public Neuron getNeuron(int i){
		return neuronList[i];
	}
	public Object3D getNeuronModel(int i){
		return neuronList[i].getModel();
	}	
	
	/*
	 * FUNCTIONS INTENDED TO BE CALLED FROM THE GUI
	 */
	
	public void toggleType(Type t,boolean b){
		if(t==Type.RS){
			showingRS = b;
		}
		else if(t==Type.FS){
			showingFS = b;
		}
		else if(t==Type.IB){
			showingIB = b;
		}
		else if(t==Type.LTS){
			showingLTS = b;
		}
		System.out.println("toggle type");
		for(int i=0;i<4;i++){
			for(int j=0;j<sections[0].length;j++){
				if(!sections[i][j].isHidden()){
					for(Integer neuron:sections[i][j].getNeurons()){
						if(getNeuron(neuron).getType()==t){
							if(b){
								showNeuron(neuron);
							}
							else{
								hideNeuron(neuron);
							}
						}
					}
				}
			}
		}
		showNetwork.checkNeurons();
		updateConnectionThreshold(connectionThreshold);
	}
	
	public Section getSectionOfNeuron(Neuron n){
		Section t = null;
		for(int j=0;j<4;j++){
			for(int i=0;i<sections[0].length;i++){
				for(Integer neuron:sections[i][j].getNeurons()){
					if(neuron == n.getID()){
						t=sections[i][j];
					}
				}
			}
		}
		return t;
	}
	
	public void updateConnectionThreshold(int threshold){
		connectionThreshold = threshold;

		clearConnections();			
		Random threshGenerator = new Random();
		
		if(selected!=null){
			pickNeuron(pickedNeuron);
		}
		else{
			for(Neuron n:neuronList){
				Connection[] connects = n.getConnections();
				for(Connection c:connects){
					int rand = threshGenerator.nextInt(99)+1;
					boolean memoryOK = true;
					if(rand<connectionThreshold && memoryOK){
						try{
							int h = c.getHead(); int t = c.getTail();
							if(neuronList[h].getModel()!=null && neuronList[t].getModel()!=null){
								c.createModel(neuronList[h].getLocation(),neuronList[t].getLocation(),false);
								showNetwork.addModel(c.getModel());
								visibleConnections.add(c);
							}
						}catch(Error e){
							//MEMORY ERROR DUE TO TOO MANY CONNECTIONS
							JOptionPane.showMessageDialog(null, "Due to a high threshold, too many connections are being made. Try a lower threshold.");
							memoryOK = false;
						}
					}
				}
			}
		}
	}
	
	public void clearConnections(){
	    if(visibleConnections!=null){
			for(Connection c : visibleConnections){
			    showNetwork.removeModel(c.getModel());		
			    c.destroyModel();
			}
			visibleConnections.clear();
	    }
	}
	
	public ArrayList<Connection>  showConnections(int id){
		Random threshGenerator = new Random();
	    ArrayList<Connection> temp = new ArrayList<Connection>();
	    //OUTGOING CONNECTIONS
	    Connection[] a = neuronList[id].getConnections();
	    for(int i=0;i<a.length;i++){
	    	int rand = threshGenerator.nextInt(99)+1;
	    	if(rand<connectionThreshold){
	    		int h = a[i].getHead(); int t = a[i].getTail();
	    		a[i].createModel(neuronList[h].getLocation(),neuronList[t].getLocation(),true);
	    		a[i].addFlag = true;
	    		showNetwork.addModel(a[i].getModel());
	    		visibleConnections.add(a[i]);
	    	}
		}	    
	    
	    //INCOMING CONNECTIONS
	    for(Neuron n:neuronList){
			Connection[] connects = n.getConnections();
			for(Connection c:connects){
		    	int rand = threshGenerator.nextInt(99)+1;
		    	if(rand<connectionThreshold){
				    if(c.getTail()==id){
						temp.add(c);
						int h = c.getHead(); int t = c.getTail();
						c.createModel(neuronList[h].getLocation(),neuronList[t].getLocation(),false);
						c.addFlag = true;
						showNetwork.addModel(c.getModel());
						visibleConnections.add(c);
				    }
		    	}
		    }
	    }
	    return temp;
	}
	
	public void highlight(String column,String layer,String color){

		String layers[] = {"II/III","IV","V","VI"};
		
		final Driver parent = this;
		int columnNumber = Integer.parseInt(column);
		
		int layerNumber = 0;
		for(int i=0;i<layers.length;i++){
			String l=layer;
			if(l.equals(layers[i])){
				layerNumber = i;
			}
		}
		
		Highlight h = new Highlight(parent,layerNumber,columnNumber,200);
		h.setTexture(color);
		extraModels.add(h);
		showNetwork.addModel(h);				
	}
	
	
	public void clearHighlights(){
		for(Object3D o:extraModels){
			showNetwork.removeModel(o);
		}
		extraModels.clear();
	}
	
	public void resetCamera(){
		showNetwork.resetCamera();
	}
	
	
	
	/*
	 * This function is called from the OptionBar class to let the driver know that
	 * the user has asked to load a new set of neuron data
	 */
	public void loadNeurons(String path){
		
		NeuronParser tempParse = new NeuronParser(this,path);
		tempParse.beginParse();
		
		maxX = tempParse.getMaxX();
		minX = tempParse.getMinX();
		maxY = tempParse.getMaxY();
		minY = tempParse.getMinY();
		maxZ = tempParse.getMaxZ();
		minZ = tempParse.getMinZ();	
		

		tempParse.sendToDriver();
		
		Connect();
		
	}
	

	public void loadTimeline(String path){
		XYSeries series = new XYSeries("Number of Spikes");
		timeGUI = new TimelineGUI(this, path);
		Timeline tline = timeGUI.getTimeline();
		int max = 0;
		for (int i = 0; i<100; i++){
			int numspikes = 0;
			for (int i1 = 1; i1<101; i1++){
				try{
					numspikes = numspikes + tline.isSpiking((i*100)+i1).size();
				}
				catch(NullPointerException e){
				}				
			}
			series.add(10*(i+1), numspikes);
			if (numspikes > max)
				max = numspikes;
		}
		XYSeriesCollection dataset = new XYSeriesCollection(series);
		histogram = ChartFactory.createXYBarChart( "Overall Activity", "ms", false, "# Spikes", 
                dataset, PlotOrientation.VERTICAL, false, false, false);
		XYPlot xyPlot = (XYPlot) histogram.getPlot();
		NumberAxis domain = (NumberAxis) xyPlot.getDomainAxis();
		NumberAxis range = (NumberAxis) xyPlot.getRangeAxis();
		domain.setRange(0, 1000);
		domain.setTickUnit(new NumberTickUnit(100));
		range.setRange(0, max);
		range.setTickUnit(new NumberTickUnit(max/15));
		XYBarRenderer renderer = (XYBarRenderer) xyPlot.getRenderer();
		renderer.setMargin(.00001);
		XYBarPainter painter = new XYBarPainter() {

	         public void paintBarShadow(Graphics2D g2, XYBarRenderer renderer, int row, int column, RectangularShape bar, RectangleEdge base, boolean pegShadow) {

	         }

	         public void paintBar(Graphics2D g2, XYBarRenderer renderer, int row, int column, RectangularShape bar, RectangleEdge base) {
	            bar.setFrame(bar.getX(), bar.getY(), bar.getWidth() + 9, bar.getHeight());
	            g2.setColor(Color.green.darker());
	            g2.fill(bar);
	            g2.draw(bar);

	         }
	      };
	      renderer.setBarPainter(painter);
		 try {
			 	ChartPanel cpanel = new ChartPanel(histogram);
			 	timeGUI.getHistoPanel().add(cpanel, BorderLayout.CENTER);
			 	timeGUI.getHistoPanel().validate();
		        ChartUtilities.saveChartAsPNG(new File("histogram.PNG"), histogram, 500, 300);
		 } 
		 catch (IOException e) {
			 e.printStackTrace();
		 }
		 timeGUI.pack();
		 timeGUI.setVisible(true);
	}
	
	public void setMidpoint(SimpleVector v){
		midPoint = v;
	}
	/*
	 * Used to determine whether or not a neuron file has already been loaded
	 */
	public boolean isInitialized(){
		return initialized;
	}
	
	/*
	 * This function will be called from the ParseNeurons class to set up an 
	 * array with the exact size of the list of neurons
	 * Also initializes the sections array
	 */
	public void initializeNeuronArray(int size,int connects){
		sections = new Section[4][5];
		connectionList = new Connection[connects];
		neuronList = new Neuron[size];
		for(int i1 = 0; i1 < 4; i1++){
			for(int i = 0; i<5; i++){
				float midlayer;
				if(i1 == 0) //Layer II/III
					midlayer = -1600*scale;
				else if(i1 == 1) //Layer IV
					midlayer = -1300*scale;
				else if(i1 == 2) //Layer V
					midlayer = -900*scale;
				else //Layer VI
					midlayer = -300*scale;
				sections[i1][i] = new Section(this, new SimpleVector((i*400+200)*scale,midlayer,200*scale),i, i1);
				sections[i1][i].hideSection();
			}
		}
		if(initialized){
			showNetwork.setLoadFile();
		}
		initialized = true;
	}
	/*
	 * This function will be called by the FileButtonListener whenever a
	 * user tries to load another file after the initial load. Disposes of the
	 * original neuronList, connectionList and sections.
	 */
	public void disposeNeuronArray(){
		clearNeuron();
		for (Neuron n : neuronList){
			showNetwork.removeModel(n.getModel());
			n.destroyModel();
		}
		for (Section[] s : sections){
			for (Section s1 : s){
				showNetwork.removeModel(s1.getModel());
				s1.destroyModel();
			}
		}
		checkNeurons();
		sections = null;
		connectionList = null;
		neuronList = null;
		options.resetSections();
	}
	
	/*
	 * This function will be called from the ParseNeurons class to insert
	 * neurons into the neuronList array and the sections array
	 */
	public void addNeuron(Neuron t){
		neuronList[t.getID()] = t;
		int layer;
		if(t.getLayer() == Layer.II || t.getLayer() == Layer.III)
			layer = 0;
		else if(t.getLayer() == Layer.IV)
			layer = 1;
		else if(t.getLayer() == Layer.V)
			layer = 2;
		else
			layer = 3;
		sections[layer][(int)((t.getX()-1)/(400*scale))].addNeuron(t); 
	}
		

		
	public void clearNeuron(){
		pickedNeuron = null;
		clearConnections();
		showNetwork.removeModel(selected);
		selected = null;
		analysis.clearNeuron();
		updateConnectionThreshold(connectionThreshold);
	}
	
	public void pickNeuron(int id){
		pickedNeuron = id;
		if(selected != null){
			showNetwork.removeModel(selected);
			selected = null;
	    }
		clearConnections();
	    clearConnect();
	    
	    highlightNeuron(id);
	   	ArrayList<Connection> incoming = showConnections(id);
	    analysis.setNeuron(neuronList[id], incoming);
	}
	
	public void clearConnect(){
		analysis.clearConnect();
	}
	public void pickConnection(Connection c){
		analysis.setConnect(c);
	}

	
	public void highlightNeuron(int id){
		selected = Primitives.getPlane(5, 15f);
                selected.setCulling(false);

                selected.setBillboarding(Object3D.BILLBOARDING_ENABLED);
    	    	selected.setTexture("select");
    	    	selected.translate(neuronList[id].getModel().getTransformedCenter());
    	    	selected.setTransparency(0);
    	    	selected.setTransparencyMode(Object3D.TRANSPARENCY_MODE_ADD);
    	    	
    	    	selected.setLighting( Object3D.LIGHTING_NO_LIGHTS);
    	    	selected.setAdditionalColor( new Color( 255,255,255) );
    	    	
    	    	selected.build();
    	    	showNetwork.addModel(selected);
	}
	
	public void Connect(){
		for (Neuron n : neuronList){

			n.setY((int)maxY-n.getY());
			
			Connection[] c =  n.getConnections();
			for (Connection h : c){
				h.setParents(neuronList[h.getHead()].getLocation(), neuronList[h.getTail()].getLocation());
				h.setScale((0.2f*scale));
			}
			
		}
		updateConnectionThreshold(OptionBar.CONNECTINIT);
	}
	
	/*
	 * This function will be called from the ParseNeurons class upon completion
	 * of parsing all the data to show the 3D neuron network.
	 */
	public void loadVisual(){
		showNetwork = new Visual(this,midPoint);
		new Thread(showNetwork).start();
	}
	
	public void showAnalysis(){
		if(analysis!=null){
			analysis.dispose();
		}
		analysis = new AnalysisBar(this);
		analysis.setVisible(true);
	}
	
	public String[] sectionData(Section s){
		String[] temp = new String[7];
			temp[0] = "@ Layer: "+layerTitles[s.getLay()]+", Column: "+s.getCol();
			int neuronTallys[] = {0,0,0,0};
			int totalConnectionCount=0;
			ArrayList<Integer> neurons = s.getNeurons();
			for(Integer i:neurons){
				neuronTallys[neuronList[i].getType().ordinal()]++;
				totalConnectionCount += neuronList[i].getConnections().length;
			}
			temp[1]="Avg Deg of Connections: "+(totalConnectionCount/s.getNeurons().size());
			temp[2]="Total Connections: "+totalConnectionCount;
			for(Type t:Type.values()){
				temp[2+t.ordinal()]=""+t+" Neuron Count: "+neuronTallys[t.ordinal()];
			}
			
		return temp;
	}
	public void showSection(int row,int column){
		System.out.println("hide row: "+row+" col: "+column);
		sections[row][column].showSection();
		checkNeurons();
		updateConnectionThreshold(connectionThreshold);
	}
	
	public Section getSection(int row,int column){
		System.out.println("Highlighted section: "+sections[row][column].getLay()+","+sections[row][column].getCol());
		return sections[row][column];
	}
	public void hideSection(int row,int column){
		sections[row][column].hideSection();
		checkNeurons();
		updateConnectionThreshold(connectionThreshold);
	}
	public void pickedObject(int objID){
	    boolean keepChecking = true;
	    
	    //CHECK SECTIONS FIRST
		for(Section[] sCol:sections){
			for(Section s:sCol){
				if(keepChecking && s.getObjectID() == objID){
					sectionData(s);
					keepChecking = false;
				}	
			}
		}
		
		//CHECK NEURONS
		if(keepChecking){
			for(Neuron n:neuronList){
				if(keepChecking && n.getModel()!=null){
					if(n.getModel().getID()==objID){
						pickNeuron(n.getID());
						keepChecking = false;
					}
				}
			}
		}
		
		//CHECK CONNECTIONS
		if(keepChecking){
			for(Connection c:visibleConnections){
				if(keepChecking && c.getModel().getID() == objID){
					pickConnection(c);
					keepChecking = false;
				}
			}
		}
		
	}
	
	public void checkNeurons(){
		showNetwork.checkNeurons();
	}
	
	public Object3D getSectionModel(int lay, int col){
		return sections[lay][col].getModel();
	}
	public Object3D getSectionGlow(int lay,int col){
		return sections[lay][col].getGlow();
	}
	public void hideNeuron(int id){
		neuronList[id].hide();
	}
	public void showNeuron(int id){
		if(showingType(neuronList[id].getType())){
			neuronList[id].show();
		}
	}
	
	public boolean showingType(Type t){
		if(t==Type.RS){
			return showingRS;
		}
		else if(t==Type.FS){
			return showingFS;
		}
		else if(t==Type.IB){
			return showingIB;
		}
		else if(t==Type.LTS){
			return showingLTS;
		}
		else{
			return false;
		}
	}
	
	/*
	 * Methods for the retrieval and modification of the scale variable
	 */
	public float getScale(){
		return scale;
	}
	/*
	 * This method will be used when we implement real-time scale changing.
	 * It changes the scale variable and then rebuilds the sections to make sure
	 * they're in the right locations.
	 */
	public void setScale(float s){
		//Needs to change the scale then rebuild the sections
		scale = s;
		for (Neuron n:neuronList){
			n.setScale(scale);
		}
		//Make new sections
		for(int i = 0; i < 5; i++){
			for(int i1 = 0; i1<4; i1++){
				float midlayer;
				if(i1 == 0) //Layer II/III
					midlayer = -200*scale;
				else if(i1 == 1) //Layer IV
					midlayer = -500*scale;
				else if(i1 == 2) //Layer V
					midlayer = -900*scale;
				else //Layer VI
					midlayer = -1500*scale;
				sections[i1][i] = new Section(this, new SimpleVector((i*400+200)*scale,midlayer,200*scale),i, i1);
			}
		}
		//then add the neurons back into them
		int layer;
		for(Neuron t:neuronList){
			if(t.getLayer() == Layer.II || t.getLayer() == Layer.III)
				layer = 0;
			else if(t.getLayer() == Layer.IV)
				layer = 1;
			else if(t.getLayer() == Layer.V)
				layer = 2;
			else
				layer = 3;
			sections[layer][(int)((t.getX()-1)/(400*scale))].addNeuron(t);
		}
		//and finally run a visibility test
		//checkSections(showNetwork.getCamera().getPosition(), true);
	}
	public void actionPerformed(ActionEvent arg0) {

	}
	public void takeScreenshot(){
		showNetwork.setScreenshot();
	}
	public float getMaxY(){
		return maxY;
	}
	public float getMinY(){
		return minY;
	}
	public float getMaxX(){
		return maxX;
	}
	public float getMinX(){
		return minX;
	}
	public float getMaxZ(){
		return maxZ;
	}
	public float getMinZ(){
		return minZ;
	}
	public TimelineGUI getTimelineGUI(){
		return timeGUI;
	}
}
